Title: How to add a dynamic image and/or a dynamic button to a DataGrid row using a DataGridTemplateColumn and a DataTemplateSelector?
Author: Jared Barneck
Email: rhyous at yahoo
Blog: http://rhyous.com
Member ID: 6636137
Language: C#, WPF
Platform: Windows
Technology: WPF, WPFToolKit, Visual Studio 2008
Level: Beginner
Description: Enter a brief description of your article
Section Platforms, Frameworks & Libraries
SubSection Windows Presentation Foundation » Controls
License: The BSD License
Introduction
This article was originally published on my blog here. I wanted to share this with CodeProject to give back as I have learned a lot from this site.
Often you want to display a DataGrid
, but you don't want to simply display it as is, you want to be able to enhance it and add functionality to it, such as adding an image to the start of each row or adding a button on each row.
So in a WPFToolkit DataGrid
you can add columns and have data or a control, such as an image or button, added to each row. Even better is that you can read the row and dynamically decide which data or control to add in each row.
For my use case, we are doing something similar to PC Doctor, where we check a bunch of values, compare them to defaults and then display a grid that show whether the state is Normal, Warning, or Error. We want to display a different image for each state. Also if the state is warning or error, we want a "Fix" button.
To start, we have a WPF project in Visual Studio 2008. We have installed the WPFToolKit and have added a reference to it in our project.
We create a table using a DataTable that looks as follows:
IntVal |
StrVal |
0 |
normal |
1 |
warning |
2 |
error |
In code, you can assign a DataTable
to the DataGrid
's DataContext
and it will do the work for you to display the DataGrid
.
As we pass this to a DataGrid
we want to add two columns:
- We want to add an image that is different if it is normal, warning, or error.
- WE want to add a button only if it is warning or error.
So the visual would look as follows:
Image |
IntVal |
StrVal |
Action |
|
0 |
normal |
|
|
1 |
warning |
<button>Fix</button> |
|
2 |
error |
<button>Fix</button> |
Step 1. Install prerequisites: Install Visual Studio 2008, and download and install the WPFToolkit.
You probably already have this done, and there are no steps for provided for these.
Step 2. Create a new WPF project in Visual studio 2008 and design the WPF interface
So once our project was created and the reference to WPFToolKit added, we then changed the XAML on our default Window1
class.
- We need to add a reference to the toolkit here as an xmlns in the window tag (See line 4).
- We need to add resources for our button. This is done in the Windows.Resources section (between lines 6 and 21). Each resource is a
DataTemplate
.
- We need to add three separate resources for our images. Again, each resource is a
DataTemplate
.
- We need to set up the
Source
value for each Image
. For now, we are going to put in static paths to image files. We also specify that no matter what the image size, we want it to display as 16x16. See the bottom of this article for more information on using images.
- We need to add a
DataGrid
from the WPFToolkit reference. (See line 23)
- We need to copnfigure the
DataGrid
's ItemSource
to use Binding
, otherwise, it won't display the DataTable
when it is assigned to the DataContext
. (See line 23)
Window1.xaml
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
|
<Window x:Class="DataGridAddButtonAndImageColumns.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpftk="http://schemas.microsoft.com/wpf/2008/toolkit"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<DataTemplate x:Key="FixThisTemplate">
<Button Name="mButtonFixThis" Click="ButtonFixThis_Click">Fix This</Button>
</DataTemplate>
<DataTemplate x:Key="NormalTemplate">
</DataTemplate>
<DataTemplate x:Key="StatusTemplateNormal" x:Name="mNormalImage">
<Image Width="16" Height="16" Source="C:\DataGridAddButtonAndImageColumns\DataGridAddButtonAndImageColumns\bin\Debug\Normal.png" />
</DataTemplate>
<DataTemplate x:Key="StatusTemplateWarning" x:Name="mWarningImage">
<Image Width="16" Height="16" Source="C:\DataGridAddButtonAndImageColumns\DataGridAddButtonAndImageColumns\bin\Debug\Warning.png" />
</DataTemplate>
<DataTemplate x:Key="StatusTemplateError" x:Name="mErrorImage">
<Image Width="16" Height="16" Source="C:\DataGridAddButtonAndImageColumns\DataGridAddButtonAndImageColumns\bin\Debug\Error.png" />
</DataTemplate>
</Window.Resources>
<Grid>
<wpftk:DataGrid Name="mDataGrid" ItemsSource="{Binding}" CanUserAddRows="False" IsReadOnly="True"></wpftk:DataGrid>
</Grid>
</Window> |
Step 3 - Create the data
The data can come from anywhere but for this basic example, we are just statically creating a DataTable
in the Constructor. This is not a document on creating a DataTable
, so we are not going to describe how it is done, but here is the code.
I will note that we add a property for the DataTable
and the DataTable.DefaultView
so that they can be easily accessed.
Data.cs
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
|
using System.Data;
namespace DataGridAddButtonAndImageColumns
{
public class Data
{
#region Member Variables
private DataTable mTable;
#endregion
#region Constructors
public Data()
{
mTable = new DataTable();
mTable.Columns.Add("IntVal", typeof(int));
mTable.Columns.Add("StrVal", typeof(string));
DataRow row0 = mTable.NewRow();
row0["IntVal"] = 0;
row0["StrVal"] = "normal";
mTable.Rows.Add(row0);
DataRow row1 = mTable.NewRow();
row1["IntVal"] = 1;
row1["StrVal"] = "warning";
mTable.Rows.Add(row1);
DataRow row2 = mTable.NewRow();
row2["IntVal"] = 2;
row2["StrVal"] = "error";
mTable.Rows.Add(row2);
}
#endregion
#region Properties
public DataTable Table
{
get { return mTable; }
set { mTable = value; }
}
public DataView View
{
get { return mTable.DefaultView; }
}
#endregion
#region Functions
#endregion
#region Enums
#endregion
}
}
|
Step 4 - Create a ViewModel that implements INotifyPropertyChanged.
So creating a ViewModel is not exactly required but there really is benefit to the Model-View-ViewModel design pattern, so we attempt to follow it even though this is a simple example application.
- We create a new class called
DataViewModel
.
- We implement the
INotifyPropertyChanged
interface: first, by adding it as an interface to our object inherits on line 7, and second by adding the functions that interface requires starting on line 44. (though for this small application it isn't used, I don't want to leave it out cause you might need it for your application.)
- We changed the constructor to take a
Data
object we designed in the previous step in as a paramter. (Line 18)
- We expose the Table and the Table's view as properties.
DataViewModel.cs
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
|
using System;
using System.ComponentModel;
using System.Data;
namespace DataGridAddButtonAndImageColumns
{
public class DataViewModel : INotifyPropertyChanged
{
#region Member Variables
readonly Data mData;
#endregion
#region Constructors
public DataViewModel(Data inData)
{
mData = inData;
}
#endregion
#region Properties
public DataView View
{
get { return mData.View; }
}
public DataTable Table
{
get { return mData.Table; }
}
#endregion
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}
|
Step 5 - Add code to pass the DataTable to the DataGrid
So in the Window1.xaml.cs
file, we create a new DataViewModel
object and pass it a new Data
object (line 12).
We then use the property in the DataViewModel
to pass the DataTable
to the DataGrid
's DataContext
property (line 20).
So now the code behind looks as follows:
Window1.xaml.cs
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
|
using System.Windows;
using System.Windows.Controls;
namespace DataGridAddButtonAndImageColumns
{
public partial class Window1 : Window
{
#region Contructor
public Window1()
{
InitializeComponent();
DataViewModel model = new DataViewModel(new Data());
mDataGrid.DataContext = model.Table;
}
#endregion
#region Functions
private void ButtonFixThis_Click(object sender, RoutedEventArgs e)
{
}
#endregion
}
}
|
Now we can compile and run see our simple app and it should display a DataGrid
with the data from the DataTable
. It will look something like this:
IntVal |
StrVal |
0 |
normal |
1 |
warning |
2 |
error |
Step 6 - Create the DataTemplateSelectors
A DataTemplateSelector
is used by the DataGridTemplateColumn
. It is the object that each DataRow
will be passed to when the DataGridTemplateColumn
is added to the DataGrid
.
Since we are going to add one column of images and a second column of buttons, we create two classes that each inherit DataTemplateSelector
.
Before we create them, we want them to share a base class, so first, we are going to create a base class for them to share. We are not just creating a base class to follow a "best practice" but we are doing this because both are going to share a function that returns the parent Window1
object. Alternately, we could copy the same function each DataTemplateSelector
but what if we update the function and forget it exists in two places? So it is good to have code only in one places.
Ok, lets step through creating this object.
- We create a new class and name it
BaseDataTemplateSelector
.
- We inherit
DataTemplateSelector
in line 7.
- We add the function to return the parent
Window1
object starting at line 19. This function basically loops through the parent objects until it finds one that is a Window1
object and once found it returns it.
BaseDataTemplateSelector.cs
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
|
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace DataGridAddButtonAndImageColumns
{
public class BaseDataTemplateSelector : DataTemplateSelector
{
#region Constructors
public BaseDataTemplateSelector()
{
}
#endregion
#region Functions
protected Window1 GetWindow1(DependencyObject inContainer)
{
DependencyObject c = inContainer;
while (true)
{
DependencyObject p = VisualTreeHelper.GetParent(c);
if (c is Window1)
{
return c as Window1;
}
else
{
c = p;
}
}
}
#endregion
}
}
|
Now that we have a base class, we create an ActionDataTemplateSelector
class for choosing whether to add a "Fix" button to each row.
We also add a StatusImageDataTemplateSelector
to choose the correct image to display for each row.
We have the ActionDataTemplateSelector
override the SelectTemplate
function (line 18). The SelectTemplate
is what is passed each DataRowView
when we add a DataGridTemplateColumn
. So in order to control the changes we make to the cells in our new column, we need to overload this function.
Here is how We create the ActionDataTemplateSelector
.
- We create a new class and name it
ActionDataTemplateSelector
.
- We have the class inherit the
BaseDataTemplateSelector
(line 7) that we created earlier. It does not have to inherit DataTemplateSelector
because the base class already inherits that.
- We override the
SelectTemplate
function starting in line 18.
- We cast the
oject
passed in as the first parameter to a DataRowView
in line 21. We know the oject
is a DataRowView
because when the DataGridTemplateColumn
is created, for each DataRowView
, the SelectTemplate
will be called. The object passed into the SelectTemplate
function will be the DataRowView
.
- We use the
GetWindow1
function to find our parent Window1
object because we need a referece to it because it holds our DataTemplates
.
- We get the value from the
DataRowView
that is contained in the IntVal column and use that to determine whether to add a button or not. (See the if statement in line 28)
- If the column is a warning or error column, we return the
DataTemplate
for the fix button and the cell in that row will hold and display the button.
ActionDataTemplateSelector.cs
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
|
using System.Data;
using System.Windows;
namespace DataGridAddButtonAndImageColumns
{
public class ActionDataTemplateSelector : BaseDataTemplateSelector
{
#region Constructors
public ActionDataTemplateSelector()
{
}
#endregion
#region Functions
public override DataTemplate
SelectTemplate(object inItem, DependencyObject inContainer)
{
DataRowView row = inItem as DataRowView;
if (row != null)
{
Window1 w = GetWindow1(inContainer);
if (row.DataView.Table.Columns.Contains("IntVal"))
{
if ((int)row["IntVal"] > 0)
{
return (DataTemplate)w.FindResource("FixThisTemplate");
}
}
return (DataTemplate)w.FindResource("NormalTemplate");
}
return null;
}
#endregion
}
}
|
The StatusImageDataTemplateSelector
also overloads the SelectTempate
function and selects the correct image for the status in much the same way that the above does, so we won't re-explain each step.
We do more calculations as each status *normal, warning, error) gets a different image, so I have three if statements starting at line 28.
StatusImageDataTemplateSelector .cs
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
|
using System.Data;
using System.Windows;
namespace DataGridAddButtonAndImageColumns
{
public class StatusImageDataTemplateSelector : BaseDataTemplateSelector
{
#region Constructors
public StatusImageDataTemplateSelector()
{
}
#endregion
#region Functions
public override DataTemplate SelectTemplate(object inItem, DependencyObject inContainer)
{
DataRowView row = inItem as DataRowView;
if (row != null)
{
if (row.DataView.Table.Columns.Contains("IntVal"))
{
Window1 w = GetWindow1(inContainer);
int status = (int)row["IntVal"];
if (status == 0)
{
return (DataTemplate)w.FindResource("StatusTemplateNormal");
}
if (status == 1)
{
return (DataTemplate)w.FindResource("StatusTemplateWarning");
}
if (status == 2)
{
return (DataTemplate)w.FindResource("StatusTemplateError");
}
}
}
return null;
}
#endregion
}
}
|
Step 7 - Create functions that add the new columns and have the constructor call each function.
Now that we have our tools in place to modify each row when we add a column, we can add code to create the two new columns. This is done in the Window1.xaml.cs
.
Each function will have four lines:
- Create a new
DataGridTemplateColumn
.
- Assign a string for the
Header
. This string will be what shows up as the column header.
- Create a new instance of each
DataTemplateSelector
object we created (the ActionDataTemplateSelector
and the StatusImageDataTemplateSelector
) and assign it to the DataGridTemplateColumn
's CellTemplateSelector
.
- Add/Insert the new
DataGridTemplateColumn
to the DataGrid
.
Below you will see the two functions that should be added to the Window1.xaml.cs
to preform the steps described
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
|
public void CreateActionButtonColumn()
{
DataGridTemplateColumn actionColumn = new DataGridTemplateColumn { CanUserReorder = false, Width = 85, CanUserSort = true };
actionColumn.Header = "Action";
actionColumn.CellTemplateSelector = new ActionDataTemplateSelector();
mDataGrid.Columns.Add(actionColumn);
}
public void CreateStatusColumnWithImages()
{
DataGridTemplateColumn statusImageColumn = new DataGridTemplateColumn { CanUserReorder = false, Width = 85, CanUserSort = false };;
statusImageColumn.Header = "Image";
statusImageColumn.CellTemplateSelector = new StatusImageDataTemplateSelector();
mDataGrid.Columns.Insert(0, statusImageColumn);
}
|
Don't forget to call the functions in the constructor. See the two calls in lines 13 and 14.
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
|
public Window1()
{
InitializeComponent();
DataViewModel model = new DataViewModel(new Data());
mDataGrid.DataContext = model.Table;
CreateActionButtonColumn();
CreateStatusColumnWithImages();
}
|
Ok, so now you are finished. This should be working for you if you compile and run the program.
Run it in debug mode and you will see it move through each object that was created as it runs through the code.
You should now have your desired output and you should now understand how to add columns with dynamic data or controls to a DataGrid.
Image |
IntVal |
StrVal |
Action |
|
0 |
normal |
|
|
1 |
warning |
<button>Fix</button> |
|
2 |
error |
<button>Fix</button> |
Options for handling the images without using a static path
The images were called statically in the above example, however, that will be problematic in actual implementation as each program is installed in a different location and the install location can usually be chosen by a user.
You have two options to resolve this, and we will show you how to do both:
- Embedding your images
- Using image files located in a relative path
Either option works. The second option makes branding a little easier as code doesn't have to be recompiled with new images to change the images, because the image files can simply be replaced.
Embedding your images
So you can embed your images as resources and use the embedded resources instead. To embed them, do this:
- In Visual Studio under your project, create a folder called Images.
- Copy your images into that folder.
- In the XAML, change each of the image resource lines as shown
<Image Width="16" Height="16" Source="Images\Warning.png" />
Using image files located in a relative path
I decided to NOT embed my images but instead solve this by using a relative path. My preference is for the images to come from actual files in an images directory that is relative to the directory from which the executable is launched:
\MyFolder\
\MyFolder\program.exe
\MyFolder\Images\
\MyFolder\Images\Normal.png
\MyFolder\Images\Warning.png
\MyFolder\Images\Error.png
So in order to use relative paths, we create another object that inherits IValueConverter
.
Here is what we do to create this:
- Create a new class called
PathConverter
.
- Make it inherit
IValueConverter
. (Line 7)
- Implement the
IValueConverter
interface's required methods (line 19 and 45).
- Add code to the
Convert
function. We only are using the Convert method and have no need to use the ConvertBack method, so we will leave it not implemented.
- Cast the
value
parameter to a DataRowView
. This should be familiar to you now as it works very similar to the DataTemplateSelector
. Each DataRowView
will be passed in as the first object
parameter.
- Get the relative path, (the path in which your executable was launched) using
System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location)
.
- Get the status value from the
DataRowView
and add a couple of if statements that return the relative path + the image file path.
PathConverter.cs
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
|
using System.Data;
using System.Globalization;
using System.Windows.Data;
namespace DataGridAddButtonAndImageColumns
{
public class PathConverter : IValueConverter
{
#region Constructors
public PathConverter()
{
}
#endregion
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
DataRowView row = value as DataRowView;
if (row != null)
{
if (row.DataView.Table.Columns.Contains("IntVal"))
{
String workingDirectory = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location);
int status = (int)row["IntVal"];
if (status == 0)
{
return workingDirectory + @"\Normal.png";
}
if (status == 1)
{
return workingDirectory + @"\Warning.png";
}
if (status == 2)
{
return workingDirectory + @"\Error.png";
}
}
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new System.NotImplementedException();
}
#endregion
}
}
|
Ok, we are not done yet. We now needed to edit the XAML again. We don't have an instance of our PathConverter
object. We can create an instance of it in the Windows.Resources
section of the XAML.
Once created, we can change the Image
objects in each DataTemplate
to bind to a converter and assign the converter to the PathConverter
object we instantiated int the XAML.
Here are the steps.
- We add an xmlns reference to load the local namespace. (Line 5)
- We add in the Windows.Resources and instance of the
PathConverter
.
- We change the Image
Source
value to:
Source="{Binding Converter={StaticResource ImagePathConverter}}"
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
|
<Window x:Class="DataGridAddButtonAndImageColumns.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpftk="http://schemas.microsoft.com/wpf/2008/toolkit"
xmlns:local="clr-namespace:DataGridAddButtonAndImageColumns"
Title="Window1" Height="300" Width="300">
<Window.Resources>
<local:PathConverter x:Key="ImagePathConverter" />
<DataTemplate x:Key="FixThisTemplate">
<Button Name="mButtonFixThis" Click="ButtonFixThis_Click">Fix This</Button>
</DataTemplate>
<DataTemplate x:Key="NormalTemplate">
</DataTemplate>
<DataTemplate x:Key="StatusTemplateNormal" x:Name="mNormalImage">
<Image Width="16" Height="16" Margin="3,0" Source="{Binding Converter={StaticResource ImagePathConverter}}" />
-->-->
</DataTemplate>
<DataTemplate x:Key="StatusTemplateWarning" x:Name="mWarningImage">
<Image Width="16" Height="16" Margin="3,0" Source="{Binding Converter={StaticResource ImagePathConverter}}" />
-->-->
</DataTemplate>
<DataTemplate x:Key="StatusTemplateError" x:Name="mErrorImage">
<Image Width="16" Height="16" Margin="3,0" Source="{Binding Converter={StaticResource ImagePathConverter}}" />
-->-->
</DataTemplate>
</Window.Resources>
<Grid>
<wpftk:DataGrid Name="mDataGrid" ItemsSource="{Binding}" CanUserAddRows="False" IsReadOnly="True"></wpftk:DataGrid>
</Grid>
</Window>
|
Ok, now we should be done.
Make sure to create the Images folder and add the images in the location where you exectuable runs. You may have to add the images folder to both the debug and release directories or otherwise resolve this, else you will get an exception when the images are not found.